56 绘制直方图
本章定位(复习与补充训练层):本章对应正课第19章《Seaborn库基础》(章节 19,分布形态的统计图形)与第20章《常用图形绘制》(章节 20,“分布用直方图/箱线图”的选型要点),并与第49章《数据框可视化》(章节 50)互为补充训练(本章为分项版),用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。
import pandas as pd # 导入Pandas数据分析库
import numpy as np # 导入NumPy数值计算库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei', 'Microsoft YaHei'] # 设置中文字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题56.1 引言分布的可视化
直方图(Histogram)是展示数据分布最直观的工具,帮助我们理解分布形态(对称、偏斜、多峰)、识别远离主分布的异常值、从尾部形态评估极端事件的概率,并比较不同资产或不同时期的分布差异。
56.2 本章学习目标
通过本章复习,你将能够:
- 说出直方图”分箱—计数—归一化”三步对应的数学量(频数、频率、密度)及其换算关系,并解释
bins过少与过多分别带来什么问题 - 用
np.log(price/price.shift(1))计算对数收益率,并说明为什么dropna()要放在收益率计算之后 - 说明
density=True与频数直方图在Y轴口径上的差异,以及”直方图总面积为1”的含义 - 复述平台任务3、任务4中堆叠(
stacked=True)与并排两种多序列直方图的画法,并判断哪种形式更适合比较A股与美股收益率的分布 - 用Q-Q图、偏度、峰度与KS/JB检验判断收益率的正态性,并解释金融收益率厚尾对风险度量的影响
先修:章节 19、章节 14(第19章的分布类图形基础与第14章的Excel数据读取,本章平台任务从Excel读入股价后计算收益率再绘图)。
56.3 直方图的数学原理
直方图是概率密度函数(PDF)的离散近似:
- 分箱(Binning):将数据范围分成 \(k\) 个区间(bin)
- 计数(Counting):统计每个区间内的数据点数
- 归一化(Normalization):转换为概率或密度
数学定义:
设数据集 \(X = \{x_1, x_2, \ldots, x_n\}\),分箱边界为 \(b_0, b_1, \ldots, b_k\):
\[ \text{count}_i = |\{x_j | b_{i-1} \leq x_j < b_i\}| \]
频率(Frequency):
\[ f_i = \frac{\text{count}_i}{n} \]
密度(Density):
\[ d_i = \frac{f_i}{b_i - b_{i-1}} = \frac{\text{count}_i}{n \cdot (b_i - b_{i-1})} \]
56.4 基础直方图
任务要求(归纳自块内任务注释与代码):从给定地址读取东方航空、上海石化 A股与美股的股价 Excel,dropna 后用 np.log(price/price.shift(1)) 计算对数收益率并再次 dropna,用 print 输出全部日收益率数据(块内的 describe() 一行未加 print,控制台不显示)。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务1(平台原始代码)
以下代码与教学平台任务要求完全一致:
# 注:平台任务代码,依赖平台指定的外部数据URL
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
import numpy as np # 导入NumPy数值计算库
# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)
stock_price = stock_price.dropna() #删除缺失值所在的行
stock_return = np.log(stock_price/stock_price.shift(1)) #计算股票收益率
stock_return = stock_return.dropna() #删除缺失值所在的行
stock_return.describe() # 查看stock_return的描述性统计量
stock_return = stock_return.dropna() #删除缺失值所在的行
print(stock_return) # 输出股票数据预期输出(数据来自教学平台的 OSS 直链,OSS 直链本机实测;以平台运行结果为准):输出为 2016-01-05 至 2019-06-28 共 821 个交易日 × 4 列的对数收益率表,作者实测的首末几行如下:
东方航空(A股) 东方航空(美股) 上海石化(A股) 上海石化(美股)
日期
2016-01-05 -0.014430 -0.005724 -0.013675 0.075940
2016-01-06 0.035693 -0.022838 0.040478 -0.004267
2016-01-07 -0.038604 -0.041165 -0.095310 -0.024441
... ... ... ... ...
2019-06-26 0.003247 0.018125 0.003884 0.017681
2019-06-27 0.019262 0.009625 0.001936 0.010109
2019-06-28 -0.003185 0.006479 -0.001936 0.004516
[821 rows x 4 columns]
任务要求(归纳自块内任务注释与代码):沿用任务一的收益率数据,在 2 行 1 列的子图中分别绘制东方航空 A股(红色)与美股(蓝色)的日收益率折线,统一 Y 轴范围为 -0.12~0.15,设置轴标签、图例(置于中上)与网格,显示图形并保存为 1.png。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务2(平台原始代码)
以下代码与教学平台任务要求完全一致:
# 注:平台任务代码,依赖平台指定的外部数据URL
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
import numpy as np # 导入NumPy数值计算库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置Matplotlib全局参数
plt.rcParams['axes.unicode_minus']=False # 设置Matplotlib全局参数
# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)
stock_price = stock_price.dropna() #删除缺失值所在的行
stock_return = np.log(stock_price/stock_price.shift(1)) #计算股票收益率
stock_return = stock_return.dropna() #删除缺失值所在的行
plt.figure(figsize=(9,9)) # 创建图形画布
plt.subplot(2,1,1) # 选择子图位置
plt.plot(stock_return["东方航空(A股)"],"r-",label=u"东方航空(A股)",lw=2) # 绘制折线图
plt.xticks(fontsize=13) # 设置X轴刻度标签
plt.yticks(fontsize=13) # 设置Y轴刻度标签
plt.ylim(-0.12,0.15) # 设置Y轴显示范围
plt.ylabel(u"收益率",fontsize=13,rotation=90) # 设置Y轴标签
plt.legend(loc=9,fontsize=13) #图例放在中上位置
plt.grid() #加入网格线
plt.subplot(2,1,2) #代表第二行的子图
plt.plot(stock_return["东方航空(美股)"],"b-",label=u"东方航空(美股)",lw=2) # 绘制折线图
plt.xticks(fontsize=13) # 设置X轴刻度标签
plt.xlabel(u"日期",fontsize=13) # 设置X轴标签
plt.yticks(fontsize=13) # 设置Y轴刻度标签
plt.ylim(-0.12,0.15) # 设置Y轴显示范围
plt.ylabel(u"收益率",fontsize=13,rotation=90) # 设置Y轴标签
plt.legend(loc=9,fontsize=13) #图例放在中上位置
plt.grid() #加入网格线
plt.show() # 显示图形
plt.savefig("1.png") # 保存图形至文件预期输出(数据来自教学平台的 OSS 直链,OSS 直链本机实测;以平台运行结果为准):本块无控制台输出,生成文件 1.png——上下两个子图分别为东方航空 A股与美股的日收益率折线,两图 Y 轴范围同为 -0.12~0.15,可直接对照两个市场的波动幅度;本机非交互后端(Agg)下”先 show 后 savefig“仍能保存出完整图形,交互环境中则应先保存后显示。块首的 from tkinter import font 为平台题面原有导入,本块并未使用。
任务要求(归纳自块内任务注释与代码):取上海石化 A股与美股两列日收益率转为数组,在 2 行 1 列的子图中分别以堆叠(stacked=True)与并排两种形式绘制直方图(bins=30,黑色边框),设置频数轴标签、图例与网格,保存为 2.png。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务3(平台原始代码)
以下代码与教学平台任务要求完全一致:
# 注:平台任务代码,依赖平台指定的外部数据URL及tkinter模块
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
from tkinter import font # 导入font模块
import pandas as pd # 导入Pandas数据分析库
import numpy as np # 导入NumPy数值计算库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置Matplotlib全局参数
plt.rcParams['axes.unicode_minus']=False # 设置Matplotlib全局参数
# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)
stock_price = stock_price.dropna() #删除缺失值所在的行
stock_return = np.log(stock_price/stock_price.shift(1)) #计算股票收益率
stock_return = stock_return.dropna() #删除缺失值所在的行
SHI_return = np.array(stock_return.iloc[:,2:]) #将上海石化A股和美股日收益率转为数组形式
plt.figure(figsize=(9,10)) # 创建图形画布
plt.subplot(2,1,1) # 选择子图位置
plt.hist(SHI_return,label=[u"上海石化A股日收益率",u"上海石化美股日收益率"],stacked=True,edgecolor="k",bins=30) #以堆叠形式展出
plt.xticks(fontsize=13) # 设置X轴刻度标签
plt.yticks(fontsize=13) # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90) # 设置Y轴标签
plt.legend(fontsize=13) # 添加图例
plt.grid() # 显示网格线
plt.subplot(2,1,2) # 选择子图位置
plt.hist(SHI_return,label=[u"上海石化A股日收益率",u"上海石化美股日收益率"],edgecolor="k",bins=30) #以并排形式展出
plt.xticks(fontsize=13) # 设置X轴刻度标签
plt.yticks(fontsize=13) # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90) # 设置Y轴标签
plt.xlabel(u"股票日收益率",fontsize=13) # 设置X轴标签
plt.legend(fontsize=13) # 添加图例
plt.grid() # 显示网格线
plt.savefig("2.png") # 保存图形至文件注:块内 from tkinter import font 导入为平台题面原有,本块并未使用。
预期输出(数据来自教学平台的 OSS 直链,OSS 直链本机实测;以平台运行结果为准):本块无控制台输出,生成文件 2.png——上子图为堆叠直方图(上海石化 A股在下、美股堆叠其上,Y 轴为两组频数之和,在 0 附近达到峰值,合计约 326),下子图为并排直方图(A股与美股相邻成对,可直接比较两个分布的中心、宽度与尾部);两图均为 30 个分箱、黑色边框。
任务要求(归纳自块内任务注释与代码):取东方航空 A股与美股两列日收益率转为数组,同样以堆叠(stacked=True)与并排两种形式在 2 行 1 列子图中绘制直方图(bins=30,黑色边框),设置频数轴标签、图例与网格,保存为 3.png。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务4(平台原始代码)
以下代码与教学平台任务要求完全一致:
# 注:平台任务代码,依赖平台指定的外部数据URL
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
import numpy as np # 导入NumPy数值计算库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置Matplotlib全局参数
plt.rcParams['axes.unicode_minus']=False # 设置Matplotlib全局参数
# 从Excel文件读取数据存入stock_price
stock_price = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx",sheet_name="Sheet1",header=0,index_col=0)
stock_price = stock_price.dropna() #删除缺失值所在的行
stock_return = np.log(stock_price/stock_price.shift(1)) #计算股票收益率
stock_return = stock_return.dropna() #删除缺失值所在的行
CEA_return = np.array(stock_return.iloc[:,0:2]) #将东方航空A股和美股收益率转为数组形式
plt.figure(figsize=(9,10)) # 创建图形画布
plt.subplot(2,1,1) # 选择子图位置
plt.hist(CEA_return,label=[u"东方航空A股日收益率",u"东方航空美股日收益率"],stacked=True,edgecolor="k",bins=30) #以堆叠形式展出
plt.xticks(fontsize=13) # 设置X轴刻度标签
plt.yticks(fontsize=13) # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90) # 设置Y轴标签
plt.legend(fontsize=13) # 添加图例
plt.grid() # 显示网格线
plt.subplot(2,1,2) # 选择子图位置
plt.hist(CEA_return,label=[u"东方航空A股日收益率",u"东方航空美股日收益率"],edgecolor="k",bins=30) #以并排形式展出
plt.xticks(fontsize=13) # 设置X轴刻度标签
plt.yticks(fontsize=13) # 设置Y轴刻度标签
plt.ylabel(u"频数",fontsize=13,rotation=90) # 设置Y轴标签
plt.xlabel(u"股票日收益率",fontsize=13) # 设置X轴标签
plt.legend(fontsize=13) # 添加图例
plt.grid() # 显示网格线
plt.savefig("3.png") # 保存图形至文件预期输出(数据来自教学平台的 OSS 直链,OSS 直链本机实测;以平台运行结果为准):本块无控制台输出,生成文件 3.png——与任务3同构,改为东方航空 A股与美股:上子图堆叠直方图、下子图并排直方图,均为 30 个分箱、黑色边框;东方航空两列收益率的取值范围约为 -0.107~0.145,分箱宽约 0.008,最高柱同样出现在 0 附近(A股列峰值约 191、美股列约 145,均在含 0 的同一分箱内)。
关键参数解析:
bins:箱数 -太少:信息损失,分布细节丢失 -太多:过度拟合,噪声干扰
- 经验法则:\(k = \sqrt{n}\) 或 \(k = \log_2(n)\)
edgecolor:箱边颜色,
'white'使分箱更清晰alpha:透明度,
0.7使图表不显沉重
56.5 概率密度与直方图
# =============================================================================
# 题目:概率密度曲线与直方图叠加
# =============================================================================
# 本任务演示如何在直方图上叠加理论概率密度曲线,检验分布拟合
# 应用场景:正态性检验、模型验证、风险评估
# ==================== 生成正态分布数据 ====================
mu, sigma = 0.001, 0.02 # mu:均值,sigma:标准差
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
data = np.random.normal(mu, sigma, 1000)
# ==================== 绘制直方图(密度) ====================
plt.figure(figsize=(10, 6))
# plt.hist()绘制直方图
# density=True:将Y轴归一化为概率密度(而非频数)
# 这样直方图的总面积为1,可以与概率密度函数对比
n, bins, patches = plt.hist(data, bins=30, density=True,
color='#2E86AB', alpha=0.7,
edgecolor='white', label='样本分布')
# n:每个区间的密度值
# bins:区间的边界
# patches:每个柱子的图形对象
# ==================== 叠加理论正态分布曲线 ====================
# np.linspace()生成等间距的序列
# data.min():起始值(数据最小值)
# data.max():结束值(数据最大值)
# 100:生成100个点
x = np.linspace(data.min(), data.max(), 100)
# 计算正态分布的概率密度函数(PDF)
# (1 / (sigma * np.sqrt(2 * np.pi))):归一化常数
# np.exp(-0.5 * ((x - mu) / sigma)**2):指数部分
pdf = (1 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - mu) / sigma)**2)
# plt.plot()绘制理论正态分布曲线
# linewidth=2.5:线宽为2.5(比普通线条粗,突出显示)
plt.plot(x, pdf, color='#E3120B', linewidth=2.5,
label='理论正态分布')
# ==================== 设置图表装饰 ====================
plt.title('收益率分布与正态拟合', fontsize=16, fontweight='bold')
plt.xlabel('收益率', fontsize=12)
plt.ylabel('概率密度', fontsize=12)
plt.legend(fontsize=11)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# ==================== 计算拟合优度 ====================
# from scipy import stats:导入SciPy的统计模块
from scipy import stats
# stats.kstest()执行Kolmogorov-Smirnov检验
# 比较经验分布与理论分布的差异
# data:样本数据
# 'norm':理论分布类型(正态分布)
# args=(mu, sigma):理论分布的参数(均值、标准差)
ks_stat, ks_pvalue = stats.kstest(data, 'norm', args=(mu, sigma))
print(f'Kolmogorov-Smirnov检验:')
print(f'统计量: {ks_stat:.4f}')
print(f'p值: {ks_pvalue:.4f}')
# p值 > 0.05:无法拒绝正态假设(数据可能服从正态分布)
# p值 ≤ 0.05:拒绝正态假设(数据不服从正态分布)
print(f'结论: {"无法拒绝正态假设" if ks_pvalue > 0.05 else "拒绝正态假设"}')正态分布的数学形式:
\[ f(x) = \frac{1}{\sigma\sqrt{2\pi}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right) \]
其中:
- \(\mu\):均值(位置参数)
- \(\sigma\):标准差(尺度参数)
拟合检验:
- KS检验:比较经验分布与理论分布
- p值 > 0.05:无法拒绝正态假设
- p值 ≤ 0.05:拒绝正态假设
56.6 分组直方图对比
# =============================================================================
# 题目:分组直方图——不同股票收益率对比
# =============================================================================
# 本任务演示如何使用子图对比多组数据的分布
# 应用场景:对比不同资产的波动率、风险分析
# ==================== 生成三只股票的收益率数据 ====================
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
stocks = {
# 贵州茅台:低波动率股票
# 均值0.001(0.1%),标准差0.018(1.8%)
'贵州茅台': np.random.normal(0.001, 0.018, 500),
# 中国平安:中等波动率股票
# 均值0.0008(0.08%),标准差0.025(2.5%)
'中国平安': np.random.normal(0.0008, 0.025, 500),
# 中小板指:高波动率股票
# 均值0.0015(0.15%),标准差0.035(3.5%)
'中小板指': np.random.normal(0.0015, 0.035, 500)
}
# ==================== 创建子图 ====================
# plt.subplots(1, 3, figsize=(15, 5))创建1行3列的子图布局
# fig:图形对象
# axes:包含3个子图对象的数组
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 定义颜色列表(红、青、深灰)
colors = ['#E3120B', '#008080', '#2C3E50']
# ==================== 循环绘制每个股票的直方图 ====================
# zip(axes, stocks.items(), colors):同时遍历子图、数据、颜色
for ax, (stock, data), color in zip(axes, stocks.items(), colors):
# ax.hist()在子图上绘制直方图
# bins=25:分成25个区间
# color=color:使用对应颜色
# alpha=0.7:透明度
# edgecolor='white':白色边框
ax.hist(data, bins=25, color=color, alpha=0.7, edgecolor='white')
# ax.axvline()在子图上绘制均值线
# data.mean():计算该股票的均值收益率
# color='black':黑色线条
# linestyle='--':虚线
# linewidth=2:线宽
ax.axvline(data.mean(), color='black', linestyle='--', linewidth=2)
# ax.set_title()设置子图标题
# f'{stock}\nμ={data.mean():.4f}, σ={data.std():.4f}':
# 显示股票名称、均值、标准差
ax.set_title(f'{stock}\nμ={data.mean():.4f}, σ={data.std():.4f}',
fontsize=12, fontweight='bold')
ax.set_xlabel('收益率', fontsize=10)
ax.set_ylabel('频数', fontsize=10)
ax.grid(axis='y', alpha=0.3)
# plt.tight_layout()自动调整子图间距
plt.tight_layout()
plt.show()
# ==================== 计算年化波动率 ====================
print('各股票波动率比较:')
for stock, data in stocks.items():
# 年化波动率 = 日波动率 * sqrt(252)
# 252:一年的交易日数量
annual_volatility = data.std() * np.sqrt(252)
print(f'{stock}: 年化波动率 = {annual_volatility:.2%}')可视化策略:
方案1:子图对比(上图)
- 优点:保持每个分布的完整性
- 缺点:难以直接比较
方案2:叠加直方图:
plt.hist(data1, bins=30, alpha=0.5, label='股票1')
plt.hist(data2, bins=30, alpha=0.5, label='股票2')- 优点:直观对比
- 缺点:颜色叠加可能混淆
方案3:核密度估计(KDE):
- 更平滑的分布曲线
- 适合多组对比
56.7 累积直方图
# =============================================================================
# 题目:累积直方图——收益率分位数
# =============================================================================
# 本任务演示如何绘制累积直方图,展示分位数信息
# 应用场景:VaR计算、收益排序、概率评估
# ==================== 生成数据 ====================
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
returns = np.random.normal(0.001, 0.02, 1000)
# ==================== 绘制累积直方图 ====================
plt.figure(figsize=(10, 6))
# plt.hist()绘制累积直方图
# cumulative=True:绘制累积分布(而非频数分布)
# density=True:归一化为概率(累积概率)
# returns:收益率数据
# bins=30:分成30个区间
plt.hist(returns, bins=30, cumulative=True, density=True,
color='#008080', alpha=0.7, edgecolor='white',
label='累积分布')
# ==================== 标注关键分位数 ====================
# 定义要标注的分位数:5%、25%、50%、75%、95%
percentiles = [5, 25, 50, 75, 95]
for p in percentiles:
# np.percentile()计算分位数
# returns:数据
# p:分位数百分比
value = np.percentile(returns, p)
# plt.axvline()绘制垂直线标注分位数
# value:分位数的值
# color='#E3120B':红色
# linestyle=':':点线
# alpha=0.6:透明度0.6
plt.axvline(value, color='#E3120B', linestyle=':', alpha=0.6)
# plt.text()在图上添加文字标签
# value, 0.5:文字位置(x坐标为分位数值,y坐标为0.5)
# f'P{p}':文字内容(如P5表示第5百分位)
# fontsize=9:字号为9
# rotation=90:文字旋转90度(垂直显示)
plt.text(value, 0.5, f'P{p}', fontsize=9, rotation=90)
# ==================== 设置图表装饰 ====================
plt.title('收益率累积分布', fontsize=16, fontweight='bold')
plt.xlabel('收益率', fontsize=12)
plt.ylabel('累积概率', fontsize=12)
plt.legend(fontsize=11)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# ==================== 输出分位数分析 ====================
print('分位数分析:')
for p in [5, 25, 50, 75, 95]:
value = np.percentile(returns, p)
# P5:有5%的概率低于此值(95% VaR)
# P25:第一四分位数
# P50:中位数
# P75:第三四分位数
# P95:有95%的概率低于此值
print(f'P{p}: {value:.4f}')累积分布函数(CDF):
\[ F(x) = P(X \leq x) = \int_{-\infty}^x f(t) dt \]
金融应用:
- VaR计算:VaR at 95% = \(F^{-1}(0.05)\)
- 收益排序:快速定位表现最好的p%时间段
- 比较分布:两个CDF图可以清晰看出差异
56.8 二维直方图
# =============================================================================
# 题目:二维直方图——两资产收益率联合分布
# =============================================================================
# 本任务演示如何绘制二维直方图,展示两个变量的联合分布
# 应用场景:相关性分析、风险分散化效果评估
# ==================== 生成相关的二维数据 ====================
# mean:两个资产的均值(股票A和股票B)
mean = [0.001, 0.0008]
# cov:协方差矩阵(2x2矩阵)
# [[0.0004, 0.0002], [0.0002, 0.0006]]:
# - 对角线:两个资产各自的方差
# - 非对角线:两个资产的协方差(相同,对称矩阵)
cov = [[0.0004, 0.0002], [0.0002, 0.0006]]
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
# np.random.multivariate_normal()生成多元正态分布随机数
# mean:均值向量
# cov:协方差矩阵
# 1000:生成1000个数据点
data_2d = np.random.multivariate_normal(mean, cov, 1000)
x = data_2d[:, 0] # 股票A收益率(第一列)
y = data_2d[:, 1] # 股票B收益率(第二列)
# ==================== 绘制二维直方图 ====================
plt.figure(figsize=(10, 8))
# plt.hist2d()绘制二维直方图
# x, y:两个变量的数据
# bins=30:每个维度分成30个区间(共30x30=900个格子)
# cmap='Blues':颜色映射(蓝色渐变,颜色越深表示频数越高)
plt.hist2d(x, y, bins=30, cmap='Blues')
# plt.colorbar()添加颜色条
# label='频数':颜色条的标签
plt.colorbar(label='频数')
# ==================== 添加均值线 ====================
# plt.axvline()绘制垂直线(股票A的均值)
plt.axvline(x.mean(), color='red', linestyle='--', linewidth=2, alpha=0.7)
# plt.axhline()绘制水平线(股票B的均值)
plt.axhline(y.mean(), color='red', linestyle='--', linewidth=2, alpha=0.7)
# ==================== 设置图表装饰 ====================
plt.title('两资产收益率联合分布', fontsize=16, fontweight='bold')
plt.xlabel('股票A收益率', fontsize=12)
plt.ylabel('股票B收益率', fontsize=12)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# ==================== 计算相关系数 ====================
# np.corrcoef()计算相关系数矩阵
# [0, 1]:取相关系数矩阵的第0行第1列(即两个变量之间的相关系数)
correlation = np.corrcoef(x, y)[0, 1]
print(f'相关系数: {correlation:.4f}')二维分布的数学含义:
联合概率密度:
\[ f_{X,Y}(x, y) \]
相关系数:
\[ \rho_{XY} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} \]
金融应用:
- 分散化效果:相关系数越接近0,分散化效果越好
- 对冲策略:负相关的资产可以相互对冲
- 因子暴露:多资产对共同因子的敏感度
56.9 核密度估计(KDE)
# =============================================================================
# 题目:核密度估计——平滑分布曲线
# =============================================================================
# 本任务演示如何使用核密度估计(KDE)平滑数据分布
# 应用场景:平滑分布曲线、非参数估计、数据分布探索
# ==================== 生成偏态分布数据 ====================
# np.random.lognormal()生成对数正态分布(右偏分布)
# 0:均值(对数尺度)
# 0.5:标准差(对数尺度)
# 1000:生成1000个数据点
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
data_skewed = np.random.lognormal(0, 0.5, 1000)
# ==================== 绘制直方图和KDE ====================
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# ==================== 左:直方图 ====================
# axes[0].hist()在第一个子图绘制直方图
# density=True:归一化为概率密度
axes[0].hist(data_skewed, bins=30, density=True,
color='#2E86AB', alpha=0.7, edgecolor='white')
axes[0].set_title('直方图(离散)', fontsize=14, fontweight='bold')
axes[0].set_xlabel('数值', fontsize=12)
axes[0].set_ylabel('密度', fontsize=12)
axes[0].grid(axis='y', alpha=0.3)
# ==================== 右:KDE ====================
# from scipy.stats import gaussian_kde:导入高斯核密度估计
from scipy.stats import gaussian_kde
# gaussian_kde()创建核密度估计对象
# data_skewed:输入数据
kde = gaussian_kde(data_skewed)
# np.linspace()生成等间距序列(用于绘制平滑曲线)
# data_skewed.min():起始值
# data_skewed.max():结束值
# 500:生成500个点
x_range = np.linspace(data_skewed.min(), data_skewed.max(), 500)
# axes[1].plot()绘制KDE曲线
# kde(x_range):计算每个点的核密度估计值
# linewidth=2.5:线宽
axes[1].plot(x_range, kde(x_range), color='#E3120B', linewidth=2.5)
# axes[1].fill_between()填充曲线下方区域
# kde(x_range):Y轴数据
# alpha=0.3:透明度
axes[1].fill_between(x_range, kde(x_range), alpha=0.3, color='#E3120B')
axes[1].set_title('核密度估计(连续)', fontsize=14, fontweight='bold')
axes[1].set_xlabel('数值', fontsize=12)
axes[1].set_ylabel('密度', fontsize=12)
axes[1].grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
# ==================== 计算偏度 ====================
print('偏度分析:')
# pd.Series(data_skewed).skew():计算偏度
# 偏度 > 0:右偏(正偏),有较长右尾
skewness_val = pd.Series(data_skewed).skew()
print(f'偏度: {skewness_val:.4f}')
print(f'结论: {"右偏分布" if skewness_val > 0 else "左偏分布" if skewness_val < 0 else "对称分布"}')核密度估计的数学原理:
\[ \hat{f}_h(x) = \frac{1}{nh}\sum_{i=1}^n K\left(\frac{x-x_i}{h}\right) \]
其中:
- \(K(\cdot)\):核函数(常用高斯核)
- \(h\):带宽(bandwidth),控制平滑程度
带宽选择:
- \(h\) 太小:过度拟合,噪声干扰
- \(h\) 太大:过度平滑,细节丢失
- 经验法则:Silverman’s rule: \(h = 1.06\sigma n^{-1/5}\)
56.10 金融应用正态性检验
# =============================================================================
# 题目:金融收益率的正态性检验
# =============================================================================
# 本任务演示如何检验收益率数据是否服从正态分布
# 应用场景:模型假设检验、风险评估、衍生品定价
# ==================== 生成真实股票收益率的典型特征(厚尾、负偏) ====================
np.random.seed(42)
n = 1000
# normal_sample:正态分布样本
# 均值为0,标准差为0.02
normal_sample = np.random.normal(0, 0.02, n)
# 添加厚尾和偏态
# np.random.standard_t(3, n):生成自由度为3的t分布(厚尾分布)
# * 0.01:缩放因子
financial_returns = normal_sample + np.random.standard_t(3, n) * 0.01
# ==================== 绘制Q-Q图(分位数-分位数图) ====================
from scipy import stats
# plt.subplots(1, 2, figsize=(14, 6))创建1行2列的子图布局
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# ==================== 左:直方图 ====================
axes[0].hist(financial_returns, bins=40, density=True,
color='#2E86AB', alpha=0.7, edgecolor='white',
label='实际分布')
# 叠加正态分布
x = np.linspace(financial_returns.min(), financial_returns.max(), 100)
# stats.norm.pdf()计算正态分布的概率密度函数
# x:x坐标
# 0:均值
# 0.02:标准差
theoretical_norm = stats.norm.pdf(x, 0, 0.02)
axes[0].plot(x, theoretical_norm, 'r-', linewidth=2,
label='理论正态')
axes[0].set_title('实际分布 vs 正态分布', fontsize=14, fontweight='bold')
axes[0].set_xlabel('收益率', fontsize=12)
axes[0].set_ylabel('密度', fontsize=12)
axes[0].legend()
axes[0].grid(axis='y', alpha=0.3)
# ==================== 右:Q-Q图 ====================
# stats.probplot()绘制概率图(Q-Q图)
# financial_returns:样本数据
# dist='norm':理论分布类型(正态分布)
# plot=axes[1]:在第二个子图上绘制
stats.probplot(financial_returns, dist='norm', plot=axes[1])
axes[1].set_title('Q-Q图(正态性检验)', fontsize=14, fontweight='bold')
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.show()
# ==================== 正态性检验 ====================
print('正态性检验结果:')
# pd.Series(financial_returns).skew():计算偏度
print(f'偏度: {pd.Series(financial_returns).skew():.4f}')
# pd.Series(financial_returns).kurtosis():计算峰度
print(f'峰度: {pd.Series(financial_returns).kurtosis():.4f}')
# ==================== Shapiro-Wilk检验 ====================
# stats.shapiro()执行Shapiro-Wilk正态性检验
# 适用于小样本(n < 5000)
# [:5000]:只取前5000个数据点(限制样本量)
shapiro_stat, shapiro_p = stats.shapiro(financial_returns[:5000])
print(f'\nShapiro-Wilk检验:')
print(f'统计量: {shapiro_stat:.4f}')
print(f'p值: {shapiro_p:.4f}')
# ==================== Jarque-Bera检验 ====================
# stats.jarque_bera()执行Jarque-Bera正态性检验
# 基于偏度和峰度
jb_stat, jb_p = stats.jarque_bera(financial_returns)
print(f'\nJarque-Bera检验:')
print(f'统计量: {jb_stat:.4f}')
print(f'p值: {jb_p:.4f}')
# p值 < 0.05:拒绝正态假设
print(f'结论: {"拒绝正态假设" if jb_p < 0.05 else "无法拒绝正态假设"}')Q-Q图原理:
如果数据服从正态分布,在Q-Q图上:
- 散点应落在红色直线上
- 偏离直线表示非正态
偏离模式:
- 尾部向上翘起:厚尾分布(金融数据典型特征)
- 左端向下:左偏(更多负的极端值)
- 右端向上:右偏(更多正的极端值)
金融意义:
- 厚尾:极端事件概率高于正态预测
- VaR低估:正态假设会低估在险价值
- 黑天鹅:需要考虑非正态分布的风险模型
56.11 本章小结
要点:
- 直方图是概率密度函数的离散近似:分箱边界 \(b_0,\ldots,b_k\),频数 \(\text{count}_i\),频率 \(f_i=\text{count}_i/n\),密度 \(d_i=f_i/(b_i-b_{i-1})\);分箱经验法则 \(k=\sqrt{n}\)
- 平台任务的数据处理链:
read_excel→dropna→np.log(p/p.shift(1))→dropna;shift(1)产生的缺失值在收益率计算之后才出现,删除时机不能提前 density=True把Y轴从频数换成概率密度,总面积为1,可与理论正态密度曲线叠加对比;cumulative=True进一步给出累积分布,图上可直接读分位数- 多序列对比:堆叠(
stacked=True)适合看合计构成,并排(默认)或半透明叠加(alpha)适合比较两个分布的位置、宽度与尾部 - 核密度估计(KDE)用平滑曲线代替离散分箱,带宽 \(h\) 过小拟合噪声、过大抹平细节;小提琴图就是箱线图与核密度的结合
- 金融收益率的典型特征是厚尾与偏态:Q-Q图两端翘起、峰度大于0、JB/Shapiro检验p值小于0.05时拒绝正态假设,此时用正态假设会低估极端风险
易错点:
- 直方图用于数值变量的分布、条形图用于分类变量的比较;直方图条形相连、条形图条形有间隔,两者不可混用
- 在堆叠直方图上比较两组数据的高度没有意义:上方序列的起点被下方序列抬高,应改用并排或同轴叠加
plt.subplot(2,1,1)与plt.subplots(2,1)是两种接口:前者按位置切换”当前子图”,后者一次性返回坐标轴数组- KS、JB检验的p值大只说明”无法拒绝正态假设”,不等于”服从正态”;样本量很大时微小偏离也会被检验出来
- 本地缺少SimHei等中文字体时,图上中文显示为方框并伴随findfont警告,属环境问题而非代码错误,以平台渲染结果为准
56.12 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
自测回忆:不看正文,写出频数、频率、密度三个量之间的换算关系;并回答
bins从10增加到100,直方图的形态会分别发生什么变化、哪一端的问题更大。参考答案(点开前请先独立完成)
解题思路:换算关系——频数 \(\text{count}_i\) 是落在第 \(i\) 箱的观测个数;频率 \(f_i = \text{count}_i / n\)(n 为总观测数);密度 \(d_i = f_i / (b_i - b_{i-1})\)(频率除以箱宽)。反推:\(f_i = d_i \times (b_i - b_{i-1})\),\(\text{count}_i = n \times f_i\);所有密度条形的面积之和为 1(不是高度之和为 1)。
bins从 10 增到 100:每箱分到的样本变少,形态从平滑轮廓变成锯齿状,出现空箱与孤柱——过拟合了抽样噪声;问题更大的是尾端:尾部本来观测就稀少,细分后每箱常常只有 0 或 1 个观测,尾部形状几乎完全由噪声决定,而主体区仍有足够样本、只是变毛糙。回扣主线:频数/频率/密度的定义与分箱经验法则见本章”直方图的数学原理”一节,选型要点另见第 章节 20 章。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断(注意第二行的精度)。
import numpy as np counts = np.array([10, 20, 40, 20, 10]) n = counts.sum() freq = counts / n print(n) print(freq.sum()) print(freq[2])参考答案(先写下你的预测再点开)
解题思路:频数合计 10+20+40+20+10 = 100;频率合计在数学上应为 1,但二进制浮点数下 0.1+0.2+0.4+0.2+0.1 的求和结果是 1.0000000000000002——这正是题面提示”注意精度”的用意;中间箱频率 40/100 = 0.4(2 的幂次除法,可精确表示)。
# 验证脚本:频数、频率与浮点求和精度 import numpy as np # 导入NumPy库 counts = np.array([10, 20, 40, 20, 10]) # 各箱频数 n = counts.sum() # 总观测数 freq = counts / n # 各箱频率 print(n) # 输出总频数 print(freq.sum()) # 频率合计:受浮点舍入影响略偏于1 print(freq[2]) # 中间箱频率预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
100 1.0000000000000002 0.4回扣主线:密度归一(各条面积和为 1)的定义见本章”概率密度与直方图”一节;频率合计出现的 1.0000000000000002 提醒浮点求和与精确值之间存在舍入差。
输出预测:下面代码模拟”先算收益率、再删缺失值”的流程,先预测每一行的输出,再上机检验。
import numpy as np import pandas as pd p = pd.Series([100.0, 110.0, 99.0, 108.9]) r = np.log(p / p.shift(1)) print(len(r), int(r.isna().sum())) print(len(r.dropna()))参考答案(先写下你的预测再点开)
解题思路:
p.shift(1)把序列整体下移一行,首行变为 NaN;对数收益率ln(p_t/p_{t-1})的首行因分母为 NaN 而为 NaN,序列长度不变(4 个),其中 1 个缺失。dropna()删除该行后剩 3 个:ln(110/100)、ln(99/110)、ln(108.9/99),约为 0.0953、−0.1054、0.0953。缺失值在收益率计算之后才出现,因此删除时机必须放在计算之后。# 验证脚本:shift产生缺失→先算后删的次序 import numpy as np # 导入NumPy库 import pandas as pd # 导入pandas库 p = pd.Series([100.0, 110.0, 99.0, 108.9]) # 4日价格序列 r = np.log(p / p.shift(1)) # shift(1)使首行分母为NaN print(len(r), int(r.isna().sum())) # 长度不变、首行缺失 print(len(r.dropna())) # 删除缺失后长度 print(r.dropna().round(4).tolist()) # 三个对数收益率预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
4 1 3 [0.0953, -0.1054, 0.0953]回扣主线:“
shift(1)的缺失在收益率计算之后才出现,删除时机不能提前”见本章”要点”第 2 条,与第 章节 19 章分布数据的预处理链一致。变式任务(平台任务同型改造):把平台任务3(或任务4)中2行1列的并排直方图改为同一坐标轴上的叠加形式——去掉分面,对两组收益率各调用一次
plt.hist,都设置density=True与alpha=0.5,再回答:哪种形式更容易比较两个市场收益率分布的宽度与尾部?参考答案(点开前请先独立完成)
解题思路:改造思路——去掉
plt.subplot(2,1,...)分面与stacked,在同一个坐标轴上对两组收益率各调用一次plt.hist,均设density=True(把两图统一归一到面积 1,消除频数口径差异)与alpha=0.5(半透明叠加,重叠区呈混合色)。判读:叠加形式更容易比较宽度与尾部——两组分布共享同一横轴,宽度差异(美股 821 个观测的标准差 0.0192 略大于 A 股的 0.0171)与尾部延伸(5%、95% 分位数美股 −0.0288、0.0295 较 A 股 −0.0245、0.0273 更向外)可以直接对着读;2×1 分面需要视线在两图间往返比对刻度,而堆叠形式的上层起点被下层抬高,高度比较没有意义。以下在平台任务的 OSS 直链数据上实跑(拉取日期 2026-08-27;若链接失效,可到教学平台运行平台任务原码,或用任意两列对数收益率演练,代码不变)。# 变式脚本:同轴叠加的双市场收益率直方图 import matplotlib.pyplot as plt # 导入绘图库 import pandas as pd # 导入pandas库 import numpy as np # 导入NumPy库 stock_price = pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/20220824/xlsx/1562273067622227968.xlsx', sheet_name='Sheet1', header=0, index_col=0) # 读取两地上市股票价格 stock_price = stock_price.dropna() # 删除缺失行 stock_return = np.log(stock_price / stock_price.shift(1)).dropna() # 对数收益率 shi_a = stock_return.iloc[:, 2] # 上海石化A股收益率 shi_us = stock_return.iloc[:, 3] # 上海石化美股收益率 print(len(shi_a), round(shi_a.std(), 4), round(shi_us.std(), 4)) # 观测数与两市场标准差 plt.hist(shi_a, bins=30, density=True, alpha=0.5, label='上海石化A股日收益率') # 密度归一并半透明 plt.hist(shi_us, bins=30, density=True, alpha=0.5, label='上海石化美股日收益率') # 同口径叠加 plt.legend() # 显示图例 plt.savefig('overlay_hist.png') # 保存图形预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
821 0.0171 0.0192图形:两个半透明直方图共享横轴,主体都在 0 附近、峰高接近,美股一侧的整体轮廓略宽、尾部略向外延,重叠区呈混合色带。
注意:以上为变式代码;列表 56.4 的平台任务仍须按原始代码原样输入教学平台。
回扣主线:“并排或半透明叠加适合比较两个分布、堆叠只看合计构成”见本章”要点”第 4 条,图形选型另见第 章节 20 章。
思考题:Q-Q图上样本分位数在两端系统性高于理论正态分位数意味着什么?若风险管理者仍以正态分布估计VaR,会高估还是低估极端损失?请从”尾部概率”的角度说明。
参考答案(点开前请先独立完成)
解题思路:Q-Q 图把样本分位数与正态理论分位数逐点对照,两端系统性高于理论值意味着样本分布的左右两尾都比正态分布更厚——同样的尾部深度处,实际观测到的分位数比正态理论预期的更极端,大幅亏损与大幅盈利都比正态假设下更常见。以正态分布估计 VaR 会低估极端损失:VaR 本质是损失分布的分位数(如 99% 置信水平取 1% 左尾分位数),正态假设把左尾概率密度压得过小,算出的分位数偏小(损失金额偏乐观),而实际序列超过该阈值的频率远高于 1%——从尾部概率的角度说,真实分布在极端区域的概率质量大于正态分布的对应概率质量,模型把”本应留在尾部”的概率挪给了中部。改进方向:用历史分位数(历史模拟法)直接刻画经验尾部,或采用极值理论(EVT)对尾部分布建模,并辅以超越 VaR 的期望损失(ES/CVaR)与压力测试。
回扣主线:Q-Q 图两端翘起与厚尾判据见本章”金融应用:正态性检验”一节,厚尾与正态假设的风险含义与第 章节 33 章一致。